Skip to content

Conversation

@ArgoZhang
Copy link
Member

@ArgoZhang ArgoZhang commented Oct 26, 2025

Link issues

fixes #7008

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Add a Gitee vote prompt to the base layout that appears on first render for non-English users and can be dismissed for 24 hours.

New Features:

  • Show a Gitee vote toast after a 10-second delay on page load for users not in the en-US locale
  • Provide a React-like RenderVote fragment with links to the Gitee voting page
  • Persist vote dismissal in localStorage to prevent re-prompting for 24 hours

Enhancements:

  • Move JS module initialization to OnAfterRenderAsync with proper first-render guard
  • Implement IAsyncDisposable in BaseLayout and asynchronously dispose the JS module

Copilot AI review requested due to automatic review settings October 26, 2025 06:26
@bb-auto bb-auto bot added the documentation Improvements or additions to documentation label Oct 26, 2025
@bb-auto bb-auto bot added this to the 9.11.0 milestone Oct 26, 2025
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Oct 26, 2025

Reviewer's Guide

This PR refactors BaseLayout to use async JS interop lifecycle methods and adds a delayed Gitee vote toast, leveraging localStorage and event handling for user interactions.

Sequence diagram for Gitee vote toast display and user interaction

sequenceDiagram
    participant Browser
    participant JSModule
    participant BaseLayout
    actor User
    Browser->>JSModule: doTask(DotNetObjectReference)
    JSModule->>Browser: initTheme()
    JSModule->>Browser: Check localStorage 'bb-gitee-vote'
    alt No recent vote
        JSModule->>Browser: setTimeout (10s)
        Browser->>BaseLayout: ShowVoteToast() (via JSInvokable)
        BaseLayout->>Browser: Show toast with vote options
    end
    User->>Browser: Click #bb-gitee-vote
    Browser->>JSModule: EventHandler.on('click', '#bb-gitee-vote')
    JSModule->>Browser: Hide toast
    JSModule->>Browser: localStorage.setItem('bb-gitee-vote', timestamp)
Loading

Class diagram for updated BaseLayout and related types

classDiagram
    class BaseLayout {
        -JSRuntime : IJSRuntime
        -WebsiteOption : IOptions<WebsiteOption>
        -Toast : ToastService
        -CommitDispatchService : DispatchService<GiteePostBody>
        -RebootDispatchService : DispatchService<bool>
        -_option : ToastOption
        -_init : bool
        -_module : JSModule
        -_interop : DotNetObjectReference_BaseLayout
        +ShowVoteToast() : Task
        +DisposeAsync() : ValueTask
        +RenderVote : RenderFragment
    }
    class ToastOption {
        Category : ToastCategory
        Title : string
        IsAutoHide : bool
        ChildContent : RenderFragment
        PreventDuplicates : bool
    }
    class DotNetObjectReference_BaseLayout
    BaseLayout --> ToastOption
    BaseLayout ..> JSModule
    BaseLayout ..> DotNetObjectReference_BaseLayout
    BaseLayout ..> ToastService
    BaseLayout ..> DispatchService_GiteePostBody
    BaseLayout ..> DispatchService_bool
    class DispatchService_GiteePostBody
    class DispatchService_bool
Loading

File-Level Changes

Change Details Files
Refactored component initialization and disposal to async JS interop
  • Migrated lifecycle hook from OnInitializedAsync to OnAfterRenderAsync with firstRender guard
  • Introduced JSModule and DotNetObjectReference fields
  • Replaced IDisposable with IAsyncDisposable and implemented async DisposeAsync to unload JS module
src/BootstrapBlazor.Server/Components/Layout/BaseLayout.razor.cs
Implemented Gitee vote toast via JS interop and markup
  • Added ShowVoteToast JSInvokable method to display a non-auto-hide toast in non-en-US environments
  • Defined RenderVote fragment in BaseLayout.razor with vote prompt and links
  • Updated BaseLayout.razor.js to export doTask and dispose functions, include localStorage timing logic and EventHandler click listener
src/BootstrapBlazor.Server/Components/Layout/BaseLayout.razor.cs
src/BootstrapBlazor.Server/Components/Layout/BaseLayout.razor.js
src/BootstrapBlazor.Server/Components/Layout/BaseLayout.razor

Assessment against linked issues

Issue Objective Addressed Explanation
#7008 Add a Gitee vote toast to the Vote component, prompting users to participate in the Gitee voting activity.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a Gitee voting activity toast notification feature that prompts users (excluding English locale users) to participate in Gitee's 2025 open source software voting. The toast appears 10 seconds after page load and remembers user interaction for 24 hours via localStorage.

Key Changes:

  • Implemented a delayed toast notification system with localStorage-based suppression logic
  • Modified the BaseLayout component lifecycle to support async disposal and JavaScript interop
  • Added event handling for vote toast interactions with automatic hiding

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
BaseLayout.razor.js Added doTask function with timer logic, localStorage checks, and event handlers for vote toast interactions
BaseLayout.razor.cs Changed from IDisposable to IAsyncDisposable, added JS module lifecycle management and ShowVoteToast method with culture check
BaseLayout.razor Added RenderVote fragment containing the voting activity UI with links to Gitee

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


private bool _init = false;
private JSModule? _module;
private DotNetObjectReference<BaseLayout>? _interop;
Copy link

Copilot AI Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _interop DotNetObjectReference is created but never disposed. Add _interop?.Dispose(); in the DisposeAsync method to prevent memory leaks.

Copilot uses AI. Check for mistakes.
if (v) {
try {
const differ = new Date().getTime() - v;
if (differ < 86400000) {
Copy link

Copilot AI Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number 86400000 (milliseconds in 24 hours) should be extracted as a named constant for better readability. Consider: const ONE_DAY_MS = 86400000;

Copilot uses AI. Check for mistakes.
const handler = setTimeout(async () => {
clearTimeout(handler);
await invoke.invokeMethodAsync("ShowVoteToast");
}, 10000);
Copy link

Copilot AI Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number 10000 (10 seconds delay) should be extracted as a named constant for better readability. Consider: const VOTE_TOAST_DELAY_MS = 10000;

Copilot uses AI. Check for mistakes.
@code {
RenderFragment RenderVote =>
@<div>
<p style="font-weight: bold;">我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!</p>
Copy link

Copilot AI Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The text '我正在参加' (I am participating) should be '我们正在参加' (We are participating) to correctly represent the project rather than an individual.

Suggested change
<p style="font-weight: bold;">我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!</p>
<p style="font-weight: bold;">我们正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!</p>

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/BootstrapBlazor.Server/Components/Layout/BaseLayout.razor.cs:160-169` </location>
<code_context>
     /// </summary>
     /// <param name="disposing"></param>
-    private void Dispose(bool disposing)
+    private async ValueTask DisposeAsync(bool disposing)
     {
         if (disposing)
         {
             CommitDispatchService.UnSubscribe(NotifyCommit);
             RebootDispatchService.UnSubscribe(NotifyReboot);
+
+            if (_module != null)
+            {
+                await _module.InvokeVoidAsync("dispose");
+                await _module.DisposeAsync();
+            }
         }
</code_context>

<issue_to_address>
**issue (bug_risk):** DisposeAsync should also dispose _interop to avoid memory leaks.

Dispose _interop in DisposeAsync if it's not null to ensure unmanaged resources are released.
</issue_to_address>

### Comment 2
<location> `src/BootstrapBlazor.Server/Components/Layout/BaseLayout.razor.js:24-26` </location>
<code_context>
+            localStorage.removeItem('bb-gitee-vote');
+        }
+    }
+    const handler = setTimeout(async () => {
+        clearTimeout(handler);
+        await invoke.invokeMethodAsync("ShowVoteToast");
+    }, 10000);
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Using setTimeout with async function may cause unhandled promise rejections.

Since errors in the async callback won't be caught, wrap the function body in try/catch or handle the promise to prevent unhandled rejections.
</issue_to_address>

### Comment 3
<location> `src/BootstrapBlazor.Server/Components/Layout/BaseLayout.razor.js:2` </location>
<code_context>
 import { getTheme, setTheme } from "../../_content/BootstrapBlazor/modules/utility.js"
+import EventHandler from "../../_content/BootstrapBlazor/modules/event-handler.js"

-export function initTheme() {
</code_context>

<issue_to_address>
**issue (complexity):** Consider replacing the EventHandler abstraction with direct event listeners and simplifying localStorage handling for clarity and efficiency.

```suggestion
// 1. Drop the custom EventHandler import and use direct add/removeEventListener
// 2. Simplify localStorage parsing & drop try/catch
// 3. Remove clearTimeout (one‐off timers auto‐clear)
//
// At top of your module:
let voteClickHandler = null;

export function doTask(invoke) {
  initTheme();

  // 2. parseInt handles invalid or missing entries without try/catch
  const lastVote = parseInt(localStorage.getItem('bb-gitee-vote'), 10);
  if (lastVote && Date.now() - lastVote < 86_400_000) {
    return;
  }

  // 3. no need to clearTimeout after one shot
  setTimeout(() => {
    invoke.invokeMethodAsync("ShowVoteToast");
  }, 10_000);

  // 1. direct listener with selector check
  voteClickHandler = (e) => {
    if (!e.target.matches('#bb-gitee-vote')) return;
    const toast = e.target.closest('.toast');
    if (toast) {
      toast.classList.remove('show');
      localStorage.setItem('bb-gitee-vote', Date.now());
    }
  };
  document.addEventListener('click', voteClickHandler);
}

export function dispose() {
  if (voteClickHandler) {
    document.removeEventListener('click', voteClickHandler);
    voteClickHandler = null;
  }
}
```
This removes the `EventHandler` abstraction, streamlines storage parsing, and keeps identical behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +24 to +26
const handler = setTimeout(async () => {
clearTimeout(handler);
await invoke.invokeMethodAsync("ShowVoteToast");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Using setTimeout with async function may cause unhandled promise rejections.

Since errors in the async callback won't be caught, wrap the function body in try/catch or handle the promise to prevent unhandled rejections.

@ArgoZhang ArgoZhang merged commit b3f199e into main Oct 26, 2025
5 checks passed
@ArgoZhang ArgoZhang deleted the doc-vote branch October 26, 2025 06:27
@codecov
Copy link

codecov bot commented Oct 26, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (09610c4) to head (a34b328).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #7009   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          741       741           
  Lines        32397     32397           
  Branches      4485      4485           
=========================================
  Hits         32397     32397           
Flag Coverage Δ
BB 100.00% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

doc(Vote): add gitee vote toast

2 participants